import {
  CommunicationWay,
  ServiceHandler,
  ServiceRegister,
} from "@main/shared/reflect";
import { TYPES } from "@main/shared/types";
import { extractErrorMessage } from "@main/utils/error-utils";
import logger from "@shared/logger/main-logger";
import { PRESET_VARIABLES_MAP } from "@shared/prompt-editor/constant";
import {
  isValidVariablePrompt,
  matchedVariables,
  restoreRenderMapByTemplate,
} from "@shared/prompt-editor/utils";
import type {
  ThreadFormChatParameters,
  UpdateThreadFormChatParametersData,
} from "@shared/triplit/types";
import { inject, injectable } from "inversify";
import { nanoid } from "nanoid";
import { match } from "ts-pattern";
import type { ThreadFormsDbService } from "./db-service/thread-forms-db-service";
import { EventNames, emitter, sendToMain } from "./event-service";
import type { ChatMessage } from "./provider-service/base-provider-service";

@injectable()
@ServiceRegister(TYPES.ThreadFormsService)
export class ThreadFormsService {
  constructor(
    @inject(TYPES.ThreadFormsDbService)
    private threadFormsDbService: ThreadFormsDbService,
  ) {
    emitter.on(EventNames.THREAD_CREATED, async ({ threadId, triggerBy }) => {
      await this.handleThreadCreated(threadId, triggerBy);
    });
    emitter.on(EventNames.THREAD_DELETED, async ({ threadId }) => {
      await this.threadFormsDbService.deleteThreadFormsByThreadId(threadId);
    });
    emitter.on(EventNames.THREAD_DELETED_ALL, async () => {
      await this.threadFormsDbService.deleteAllThreadForms();
    });
  }

  private async handleThreadCreated(
    threadId: string,
    triggerBy: "first-send-message" | "without-send-message",
  ) {
    match(triggerBy)
      .with(
        "first-send-message",
        async () =>
          await this.threadFormsDbService.updateChatParametersFromCache(
            threadId,
          ),
      )
      .with(
        "without-send-message",
        async () =>
          await this.threadFormsDbService.updateChatParametersByThreadId(
            threadId,
            {
              chatParameters: {},
            },
          ),
      )
      .otherwise(() => {
        throw new Error("Invalid triggerBy");
      });
  }

  async getChatParametersByThreadId(
    threadId: string,
  ): Promise<ThreadFormChatParameters> {
    try {
      return this.threadFormsDbService.getChatParametersByThreadId(threadId);
    } catch (error) {
      logger.error("ThreadFormsService:getChatParametersByThreadId error", {
        error,
      });
      throw error;
    }
  }

  async renderTemplateByThreadId(
    threadId: string,
    messages: ChatMessage[],
  ): Promise<ChatMessage[]> {
    try {
      const lastMsg = messages.at(-1);
      if (!lastMsg) return messages;
      const rawInput = lastMsg.content;
      const chatParameters = await this.getChatParametersByThreadId(threadId);
      const template = chatParameters?.userPromptTemplate ?? "";
      const isValid = isValidVariablePrompt(template);
      if (!isValid || !template) return messages;

      const variables = matchedVariables(template);

      let modelId = "";
      if (variables.includes("model_id")) {
        modelId = await this.threadFormsDbService.getThreadModelName(threadId);
      }

      const renderMap = {};

      const renderedContent = variables.reduce((acc, variable) => {
        const renderer = PRESET_VARIABLES_MAP.get(variable)?.renderTemplate;
        if (!renderer) return acc;

        const handler = match(variable)
          .with("input", () => () => rawInput)
          .with("model_id", () => () => modelId)
          .otherwise(() => undefined);

        const rendered = renderer(acc, handler);
        renderMap[variable] = rendered.value;

        return rendered.replaced;
      }, template);

      sendToMain(EventNames.MESSAGE_RENDERED, {
        messageId: lastMsg.id,
        template,
        renderedContent,
        variables,
        renderMap,
      });

      return messages.with(-1, {
        ...lastMsg,
        renderedContent,
      });
    } catch (error) {
      logger.error("ThreadFormsService:renderTemplateByThreadId error", {
        error,
      });
      throw error;
    }
  }

  async renderMessagesBySystemPrompt(
    threadId: string,
    messages: ChatMessage[],
  ): Promise<ChatMessage[]> {
    const {
      systemPrompt,
      systemPromptRenderMap,
      systemPromptVariables,
      systemPromptRenderedContent,
    } = await this.threadFormsDbService.getSystemPromptParams(threadId);

    if (systemPromptRenderedContent) {
      if (!systemPromptVariables.has("now")) {
        const systemPromptObj = {
          id: nanoid(),
          role: "system" as const,
          content: systemPromptRenderedContent,
        };

        return [systemPromptObj, ...messages];
      }

      const renderedContent = restoreRenderMapByTemplate(
        systemPrompt,
        systemPromptRenderMap,
      );
      if (!renderedContent) return messages;

      const systemPromptObj = {
        id: nanoid(),
        role: "system" as const,
        content: renderedContent,
      };

      return [systemPromptObj, ...messages];
    }

    const renderMap = {};
    let renderedContent = "";
    const variables = matchedVariables(systemPrompt);

    let modelId = "";
    if (variables.includes("model_id")) {
      modelId = await this.threadFormsDbService.getThreadModelName(threadId);
    }
    renderedContent = variables.reduce((acc, variable) => {
      const renderer = PRESET_VARIABLES_MAP.get(variable)?.renderTemplate;
      if (!renderer) return acc;

      const handler = match(variable)
        .with("model_id", () => () => modelId)
        .otherwise(() => undefined);

      const rendered = renderer(acc, handler);
      renderMap[variable] = rendered.value;

      return rendered.replaced;
    }, systemPrompt);

    await this.threadFormsDbService.updateSystemPromptParams(threadId, {
      systemPromptRenderMap: renderMap,
      systemPromptRenderedContent: renderedContent,
      systemPromptVariables: new Set(variables),
    });

    const systemPromptObj = {
      id: nanoid(),
      role: "system" as const,
      content: renderedContent,
    };

    return [systemPromptObj, ...messages];
  }

  @ServiceHandler(CommunicationWay.RENDERER_TO_MAIN__TWO_WAY)
  async updateChatParametersByThreadId(
    _event: Electron.IpcMainEvent,
    threadId: string,
    updateChatParametersData: UpdateThreadFormChatParametersData,
  ): Promise<{
    isOk: boolean;
    errorMsg: string | null;
  }> {
    try {
      await this.threadFormsDbService.updateChatParametersByThreadId(
        threadId,
        updateChatParametersData,
      );

      return { isOk: true, errorMsg: null };
    } catch (error) {
      logger.error("ThreadFormsService:updateChatParametersByThreadId error", {
        error,
      });
      return { isOk: false, errorMsg: extractErrorMessage(error) };
    }
  }
}
